package com.ronsoft.books.nio.regex;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A file searching class, similar to grep, which returns information about
 * lines matched in the specified files. Instances of this class * are tied to a
 * specific regular expression pattern and may be applied * repeatedly to
 * multiple files. Instances of Grep are thread safe, they may be shared.
 * 
 * @author Michael Daudel (mgd@ronsoft.com) (original)
 * @author Ron Hitchens (ron@ronsoft.com) (hacked)
 */
public class Grep {
    // the pattern to use for this instance
    private Pattern pattern;

    /**
     * Instantiate a Grep object for the given pre-compiled Pattern object.
     * 
     * @param pattern
     *            A java.util.regex.Pattern object specifying the pattern to
     *            search for.
     */
    public Grep(Pattern pattern) {
        this.pattern = pattern;
    }

    /**
     * Instantiate a Grep object and compile the given regular expression
     * string.
     * 
     * @param regex
     *            The regular expression string to compile into a Pattern for
     *            internal use.
     * @param ignoreCase
     *            If true, pass Pattern.CASE_INSENSITIVE to the Pattern
     *            constuctor so that seaches will be done without regard to
     *            alphabetic case. Note, this only applies to the ASCII
     *            character set. Use embedded expressions to set other options.
     */
    public Grep(String regex, boolean ignoreCase) {
        this.pattern = Pattern.compile(regex, (ignoreCase) ? Pattern.CASE_INSENSITIVE : 0);

    }

    /**
     * Instantiate a Grep object with the given regular expression string, with
     * default options.
     */
    public Grep(String regex) {
        this(regex, false);
    }

    // ---------------------------------------------------------------

    /**
     * Perform a grep on the given file.
     * 
     * @param file
     *            A File object denoting the file to scan for the regex given
     *            when this Grep instance was constructed.
     * @return A type-safe array of Grep.MatchedLine objects describing the
     *         lines of the file matched by the pattern.
     * @exception IOException
     *                If there is a problem reading the file.
     */
    public MatchedLine[] grep(File file) throws IOException {
        List<MatchedLine> list = grepList(file);
        MatchedLine matches[] = new MatchedLine[list.size()];

        list.toArray(matches);

        return (matches);
    }

    /**
     * Perform a grep on the given file.
     * 
     * @param file
     *            A String filename denoting the file to scan for the regex
     *            given when this Grep instance was constructed.
     * @return A type-safe array of Grep.MatchedLine objects describing the
     *         lines of the file matched by the pattern.
     * @exception IOException
     *                If there is a problem reading the file.
     */
    public MatchedLine[] grep(String fileName) throws IOException {
        return (grep(new File(fileName)));
    }

    /**
     * Perform a grep on the given list of files. If a given file cannot be
     * read, it will be ignored as if empty.
     * 
     * @param files
     *            An array of File objects to scan.
     * @return A type-safe array of Grep.MatchedLine objects describing the
     *         lines of the file matched by the pattern.
     */
    public MatchedLine[] grep(File[] files) {
        List<Object> aggregate = new LinkedList<Object>();

        for (int i = 0; i < files.length; i++) {
            try {
                List<?> temp = grepList(files[i]);

                aggregate.addAll(temp);
            } catch (IOException e) {
                // ignore I/O exceptions
            }
        }

        MatchedLine matches[] = new MatchedLine[aggregate.size()];

        aggregate.toArray(matches);

        return (matches);
    }

    // -------------------------------------------------------------

    /**
     * Encapsulation of a matched line from a file. This immutable object has
     * five read-only properties:
     * <ul>
     * <li>getFile(): The File this match pertains to.</li>
     * <li>getLineNumber(): The line number (1-relative) within the file where
     * the match was found.</li>
     * <li>getLineText(): The text of the matching line</li>
     * <li>start(): The index within the line where the matching pattern begins.
     * </li>
     * <li>end(): The index, plus one, of the end of the matched character
     * sequence.</li>
     * </ul>
     */
    public static class MatchedLine {
        private File file;
        private int lineNumber;
        private String lineText;
        private int start;
        private int end;

        MatchedLine(File file, int lineNumber, String lineText, int start, int end) {
            this.file = file;
            this.lineNumber = lineNumber;
            this.lineText = lineText;
            this.start = start;
            this.end = end;
        }

        public File getFile() {
            return (this.file);
        }

        public int getLineNumber() {
            return (this.lineNumber);
        }

        public String getLineText() {
            return (this.lineText);
        }

        public int start() {
            return (this.start);
        }

        public int end() {
            return (this.end);
        }
    }

    // -----------------------------------------------------------

    /**
     * Run the grepper on the given File.
     * 
     * @return A (non-type-safe) List of MatchedLine objects.
     */
    private List<MatchedLine> grepList(File file) throws IOException {
        if (!file.exists()) {
            throw new IOException("Does not exist: " + file);
        }

        if (!file.isFile()) {
            throw new IOException("Not a regular file: " + file);
        }

        if (!file.canRead()) {
            throw new IOException("Unreadable file: " + file);
        }

        LinkedList<MatchedLine> list = new LinkedList<MatchedLine>();
        FileReader fr = new FileReader(file);
        LineNumberReader lnr = new LineNumberReader(fr);
        Matcher matcher = this.pattern.matcher("");
        String line;

        while ((line = lnr.readLine()) != null) {
            matcher.reset(line);

            if (matcher.find()) {
                list.add(new MatchedLine(file, lnr.getLineNumber(), line, matcher.start(), matcher.end()));
            }
        }

        lnr.close();

        return (list);
    }

    // ---------------------------------------------------------------

    /**
     * Test code to run grep operations. Accepts two command-line options: -i or
     * --ignore-case, compile the given pattern so that case of alpha characters
     * is ignored. Or -1, which runs the grep operation on each individual file,
     * rather that passing them all to one invocation. This is just to test the
     * different methods. The printed ouptut is slightly different when -1 is
     * specified.
     */
    public static void main(String[] argv) {
        // Set defaults
        boolean ignoreCase = false;
        boolean onebyone = false;
        List<String> argList = new LinkedList<String>(); // to gather args

        // Loop through the args, looking for switches and saving
        // the patterns and filenames
        for (int i = 0; i < argv.length; i++) {
            if (argv[i].startsWith("-")) {
                if (argv[i].equals("-i") || argv[i].equals("--ignore-case")) {
                    ignoreCase = true;
                }

                if (argv[i].equals("-1")) {
                    onebyone = true;
                }

                continue;
            }

            // not a switch, add it to the list
            argList.add(argv[i]);
        }

        // Enough args to run?
        if (argList.size() < 2) {
            System.err.println("usage: [options] pattern filename ...");
            return;
        }

        // First arg on the list will be taken as the regex pattern.
        // Pass the pattern to the new Grep object, along with the
        // current value of the ignore case flag.
        Grep grepper = new Grep((String) argList.remove(0), ignoreCase);

        // somewhat arbitrarily split into two ways of calling the
        // grepper and printing out the results
        if (onebyone) {
            Iterator<String> it = argList.iterator();

            // Loop through the filenames and grep them
            while (it.hasNext()) {
                String fileName = it.next();

                // Print the filename once before each grep
                System.out.println(fileName + ":");

                MatchedLine[] matches = null;

                // Catch exceptions
                try {
                    matches = grepper.grep(fileName);
                } catch (IOException e) {
                    System.err.println("\t*** " + e);
                    continue;
                }

                // Print out info about the matched lines
                for (int i = 0; i < matches.length; i++) {
                    MatchedLine match = matches[i];

                    System.out.println("  " + match.getLineNumber() + " [" + match.start() + "-" + (match.end() - 1) + "]: " + match.getLineText());
                }
            }
        } else {
            // Convert the filename list to an array of File
            File[] files = new File[argList.size()];

            for (int i = 0; i < files.length; i++) {
                files[i] = new File((String) argList.get(i));
            }

            // Run the grepper; unreadable files are ignored
            MatchedLine[] matches = grepper.grep(files);

            // Print out info about the matched lines
            for (int i = 0; i < matches.length; i++) {
                MatchedLine match = matches[i];

                System.out.println(match.getFile().getName() + ", " + match.getLineNumber() + ": " + match.getLineText());
            }
        }
    }
}
